package hirondelle.predict.main.prediction;

import static hirondelle.predict.main.prediction.PredictionAction.ADD_PREDICTION;
import static hirondelle.predict.main.prediction.PredictionAction.CHANGE_PREDICTION;
import static hirondelle.predict.main.prediction.PredictionAction.DELETE_PREDICTION;
import static hirondelle.predict.main.prediction.PredictionAction.FETCH_OWNER;
import static hirondelle.predict.main.prediction.PredictionAction.FETCH_PREDICTION;
import static hirondelle.predict.main.prediction.PredictionAction.LIST_PREDICTIONS;
import hirondelle.web4j.database.DAOException;
import hirondelle.web4j.database.Db;
import hirondelle.web4j.database.DbTx;
import hirondelle.web4j.database.DuplicateException;
import hirondelle.web4j.database.Tx;
import hirondelle.web4j.database.TxTemplate;
import hirondelle.web4j.model.Id;
import hirondelle.web4j.model.DateTime;
import hirondelle.web4j.util.Util;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
import java.util.logging.Logger;

/**  Data Access Object (DAO) for {@link Prediction} objects. */
public final class PredictionDAO {
  
  /** Return a <tt>List</tt> of all {@link Prediction} objects in a specified Prediction List.  */
  public List<Prediction> list(Id aPredictionListId) throws DAOException {
    return Db.list(Prediction.class, LIST_PREDICTIONS, aPredictionListId);
  }
  
  /** Return a single {@link Prediction} identified by its id, and its Prediction List id.  */
  Prediction fetch(Id aPredictionId, Id aPredictionListId) throws DAOException {
    return Db.fetch(Prediction.class, FETCH_PREDICTION, aPredictionId, aPredictionListId);
  }
  
  /**
   Add a new {@link Prediction} to the database.
   @return the autogenerated database id, if any.
  */
  Id add(Prediction aPrediction, Id aPredictionListId, DateTime aNow) throws DAOException, DuplicateException {
    Object[] params = {
      aPredictionListId,  aPrediction.getText(), aNow, 
      aPrediction.getRemark(), getOutcomeId(aPrediction), getOutcomeDateForInsert(aPrediction, aNow)
    };
    return Db.add(ADD_PREDICTION, params);
  }
  
  /** 
   Update an existing {@link Prediction}.
   <P>The outcome date is set only when the outcome itself has changed from its previous value.
   @return <tt>true</tt> only if the edit is executed successfullly. 
   */
  boolean change(Prediction aPrediction, Id aPredictionListId, DateTime aToday) throws DAOException, DuplicateException {
    Tx changeTx = new Change(aPrediction, aPredictionListId, aToday);
    return Util.isSuccess(changeTx.executeTx());
  }
  
  /**
   Delete a {@link Prediction}.
  */
  void delete(Id aPredictionId, Id aPredictionListId) throws DAOException {
    Db.delete(DELETE_PREDICTION, aPredictionId, aPredictionListId);
  }
  
  /**
   Return the login name of the user that owns the given prediction list
   @param aParentId identifies the prediction list.
   */
  Id fetchLoginNameFor(Id aParentId) throws DAOException {
    return Db.fetchValue(Id.class, FETCH_OWNER, aParentId);
  }
  
  // PRIVATE 
  private static final Logger fLogger = Util.getLogger(PredictionDAO.class);

  /** Return the current date only if there is an outcome present. */
  private DateTime getOutcomeDateForInsert(Prediction aPrediction, DateTime aNow){
    return aPrediction.getOutcome() != null ? aNow.truncate(DateTime.Unit.DAY) : null; 
  }
  
  /** 
   The outcome date is calculated from the old record and the new one.
   If there is a change in the outcome, then a new outcome date is applied. 
   Otherwise, the outcome date is coerced to the old one (which may be null).  
  */
  private DateTime getOutcomeDate(Prediction aNew, Prediction aOld, DateTime aToday){
    DateTime result = aOld.getOutcomeDate(); //may be null
    if (hasChangedOutcome(aOld, aNew)) {
      if( aNew.getOutcome() != null ){
        result = aToday;
      }
      else {
        result = null;
      }
    }
    return result;
  }
  
  /** 
   Return true only if the outcome of the new prediction differs from that of the old, in any way. 
  */
  private boolean hasChangedOutcome(Prediction aOld, Prediction aNew){
    boolean result = false;
    if ( aOld.getOutcome() == null && aNew.getOutcome() != null ) {
      result = true;
    }
    else if (aOld.getOutcome() != null && aNew.getOutcome() == null) {
      result = true;
    }
    else if (aOld.getOutcome() != null && aNew.getOutcome() != null){
      if (! aNew.getOutcome().getId().equals(aOld.getOutcome().getId())){
        result = true;
      }
    }
    fLogger.fine("Has changed outcome: " + result);
    return result;
  }
  
  /** 
   For the change operation, the state of the outcome is simply applied as-is. 
  */
  private Id getOutcomeId(Prediction aPrediction){
    return aPrediction.getOutcome() != null ? aPrediction.getOutcome().getId() : null;
  }

  /** 
   This transaction ensures that the change operation happens as a single unit of work.
   If triggers are available in the database, then they should likely be used instead of this method. 
   They would likely be more elegant than this style.
  */ 
  private final class Change extends TxTemplate {
    Change(Prediction aNewPrediction, Id aPredictionListId, DateTime aToday){
      fNew = aNewPrediction;
      fListId = aPredictionListId;
      fToday = aToday;
    }
    @Override public int executeMultipleSqls(Connection aConnection) throws SQLException, DAOException {
      fLogger.fine("Executing transaction for update operation.");
      Prediction old = DbTx.fetch(aConnection, Prediction.class, FETCH_PREDICTION, fNew.getId(), fListId);
      Object[] params = {
        fNew.getText(), fNew.getRemark(), getOutcomeId(fNew), getOutcomeDate(fNew, old, fToday),
        fNew.getId(), fListId 
      } ;
      return DbTx.edit(aConnection, CHANGE_PREDICTION, params);
    }
    private Prediction fNew;
    private Id fListId;
    private DateTime fToday;
  }
}